feat(sidebar-v2): thread snoozing#4311
Conversation
Adds an inbox-zero snooze lifecycle to Sidebar v2: hide a thread until a chosen time, then bring it back loudly. Contracts + server: - thread.snooze / thread.unsnooze commands and thread.snoozed / thread.unsnoozed events; snoozedUntil/snoozedAt on thread + shell (optional, so pre-snooze payloads still decode) - decider invariants: wake time must be in the future; blocked-on-you work (open approval/user-input) cannot be snoozed; duplicates re-emit idempotently; a user message on a snoozed thread spends the return ticket (thread.unsnoozed reason "activity") - migration 034 adds snoozed_until/snoozed_at to projection_threads; projector, projection pipeline, and all snapshot/shell queries thread the fields through - threadSnooze capability flag, version-skew gated like threadSettlement Shared logic (client-runtime): - effectiveSnoozed: hidden while the wake time is ahead AND the thread hasn't raised its hand; timer wakes are derived (no server event) - threadRaisedHandWhileSnoozed: pending approval/input, a failure newer than the snooze, or a run completed after the snooze wake early — classification only, without clearing the server-side snooze - threadWokeAt drives the woke indicator until the user visits Sidebar v2 UI: - hover clock button beside Settle opening a preset popover (In 1 hour / This evening / Tomorrow / Next week; event-based options teased as SOON) - collapsed "Snoozed · N" shelf between Active and Settled — never hides threads without a trace; expanded rows show wake countdowns and a wake-now button; shelf disappears at count 0 - amber "Woke" pill + unread-weight title when a snooze ends; the static sort is untouched, the pill carries the signal - context-menu Snooze submenu (single + multi-select), undo toast Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
ApprovabilityVerdict: Needs human review This PR introduces a significant new feature (thread snoozing) with database migrations, new server-side commands/events, and substantial client-side UI changes including new components and state management. New features of this scope warrant human review. You can customize Macroscope's approvability policy. Learn more. |
Merge resolves the SidebarV2 restyle (#4268) by re-applying snooze onto the new surface model (sidebar tokens, thread tooltips, restyled action buttons), and folds in serverSelfUpdate capability alongside threadSnooze. Review fixes: - Snooze classification now uses a precise clock instead of the minute-quantized partition time, plus a timeout armed at the earliest upcoming wake — the shelf empties the moment a snooze expires instead of up to a minute late (Cursor) - thread.snooze now rejects queued turn starts exactly like thread.settle, server-side (shared threadHasQueuedTurnStart helper) and client-side (canSnooze gains the hasQueuedTurnStart check) (Cursor) - threadWokeAt keeps an early hand-raise wake authoritative after the scheduled wake time passes, so a visited-and-cleared Woke pill cannot resurface when snoozedUntil elapses (Macroscope) - resolveSnoozePresets advances calendar days with setDate instead of DAY_MS offsets, fixing tomorrow/next-week presets across DST transitions (Macroscope) - Documented why hasOpenBlockingRequest's scan of the 500-capped activities array is sound for the snooze invariant (Macroscope) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Updated the branch (merged main, incl. the #4268 sidebar restyle — snooze UI re-applied onto the new surface tokens) and addressed all four review findings:
All suites green: server 1597, web 1450, client-runtime 469, contracts 187; repo-wide typecheck + lint clean. 🤖 Generated with Claude Code |
Merge folds the snooze thread tooltip into main's reworked SidebarV2ThreadTooltip (glass styling, branchMismatch row, no status prop). Review fix (Cursor): isWoke, snoozeWakeLabel, and snoozeWakeDescription now parse ISO timestamps through the shared parseTimestampDate utility (newly exported) instead of raw Date.parse/new Date, so an unparseable lastVisitedAt counts as never-visited rather than eating the Woke pill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Branch updated again — merged latest main (the tooltip rework landed there, so the snooze tooltip now rides
Typecheck, lint, and the web suite (1471 tests) are green; PR is mergeable again. 🤖 Generated with Claude Code |
- threadWokeAt: drop the redundant effectiveSnoozed call and re-parse — one classification pass; the tail collapses to a single timer-elapsed expression - SnoozePopoverButton: fully controlled by the row's existing snoozeMenuOpen state instead of mirroring it in a child useState - Multi-select snooze: drop the snoozableThreads.length === count guard (threadKeys is already filtered against the same map with no intervening mutation) - decider thread.snooze: hoist existingSnoozedAt so the payload doesn't re-check thread.snoozedAt for narrowing - Un-export SnoozePresetId (no external consumers) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/server/src/orchestration/decider.snoozed.test.ts (1)
146-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winGuarded payload assertions can pass vacuously.
The payload checks only execute inside
if (events[0]?.type === "thread.snoozed"); if the decider ever stopped emittingthread.snoozed(or emitted nothing), this test would still pass silently. Add an unconditionalexpect(events[0]?.type).toBe("thread.snoozed")(and a length assertion here) so the narrowing guard becomes redundant rather than load-bearing. The idempotent-duplicate test at Lines 137-142 has the same pattern.💚 Proposed assertion hardening
const events = Array.isArray(event) ? event : [event]; + expect(events).toHaveLength(1); + expect(events[0]?.type).toBe("thread.snoozed"); if (events[0]?.type === "thread.snoozed") { expect(events[0].payload.snoozedUntil).toBe("1970-01-03T09:00:00.000Z"); expect(events[0].payload.updatedAt).not.toBe(NOW); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/src/orchestration/decider.snoozed.test.ts` around lines 146 - 163, Harden the assertions in the “re-snoozing to a DIFFERENT wake time stamps fresh” test and the idempotent-duplicate test by asserting the expected event array length and unconditionally asserting events[0]?.type is "thread.snoozed" before checking payload fields. Remove the conditional type guards so payload assertions cannot pass vacuously, while preserving the existing expected payload checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/components/SidebarV2.tsx`:
- Around line 1178-1187: The snooze wake timer in the useEffect must cap delayMs
at the maximum 32-bit setTimeout delay of 2,147,483,647 ms. Preserve the
existing non-negative calculation and cleanup behavior while clamping far-future
snoozedUntil values before calling window.setTimeout.
---
Nitpick comments:
In `@apps/server/src/orchestration/decider.snoozed.test.ts`:
- Around line 146-163: Harden the assertions in the “re-snoozing to a DIFFERENT
wake time stamps fresh” test and the idempotent-duplicate test by asserting the
expected event array length and unconditionally asserting events[0]?.type is
"thread.snoozed" before checking payload fields. Remove the conditional type
guards so payload assertions cannot pass vacuously, while preserving the
existing expected payload checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 272338c3-d45e-42a8-9022-197757f034bb
📒 Files selected for processing (29)
apps/mobile/src/state/use-thread-selection.tsapps/server/src/environment/ServerEnvironment.tsapps/server/src/orchestration/Layers/ProjectionPipeline.tsapps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.tsapps/server/src/orchestration/Layers/ProjectionSnapshotQuery.tsapps/server/src/orchestration/Schemas.tsapps/server/src/orchestration/decider.snoozed.test.tsapps/server/src/orchestration/decider.tsapps/server/src/orchestration/projector.test.tsapps/server/src/orchestration/projector.tsapps/server/src/persistence/Layers/ProjectionRepositories.test.tsapps/server/src/persistence/Layers/ProjectionThreads.tsapps/server/src/persistence/Migrations.tsapps/server/src/persistence/Migrations/034_ProjectionThreadsSnoozed.tsapps/server/src/persistence/Services/ProjectionThreads.tsapps/web/src/components/Sidebar.snooze.test.tsapps/web/src/components/Sidebar.snooze.tsapps/web/src/components/SidebarV2.tsxapps/web/src/hooks/useThreadActions.tsapps/web/src/state/entities.tsapps/web/src/timestampFormat.tspackages/client-runtime/src/operations/commands.tspackages/client-runtime/src/state/threadCommands.tspackages/client-runtime/src/state/threadDetail.tspackages/client-runtime/src/state/threadReducer.tspackages/client-runtime/src/state/threadSettled.tspackages/client-runtime/src/state/threadSnoozed.test.tspackages/contracts/src/environment.tspackages/contracts/src/orchestration.ts
| useEffect(() => { | ||
| const nextWakeAtMs = | ||
| snoozedThreads.length > 0 && snoozedThreads[0]?.snoozedUntil != null | ||
| ? Date.parse(snoozedThreads[0].snoozedUntil) | ||
| : Number.NaN; | ||
| if (Number.isNaN(nextWakeAtMs)) return; | ||
| const delayMs = Math.max(0, nextWakeAtMs - Date.now()) + 50; | ||
| const id = window.setTimeout(() => bumpSnoozeWakeTick((tick) => tick + 1), delayMs); | ||
| return () => window.clearTimeout(id); | ||
| }, [snoozedThreads]); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Look for any max-snooze / horizon validation on snoozedUntil in the decider/contracts.
rg -nP 'snoozedUntil' --type=ts -C3 apps/server/src/orchestration packages/contracts/src | rg -nP -i 'max|horizon|clamp|limit|valid|Date\.parse|throw' -C2Repository: pingdotgg/t3code
Length of output: 687
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant file and inspect the snooze wake effect plus the snoozed thread ordering.
ast-grep outline apps/web/src/components/SidebarV2.tsx --view expanded | sed -n '1,220p'
printf '\n--- effect slice ---\n'
sed -n '1160,1205p' apps/web/src/components/SidebarV2.tsx
printf '\n--- snoozedThreads references ---\n'
rg -n "snoozedThreads" apps/web/src/components/SidebarV2.tsx
printf '\n--- server invariant context ---\n'
sed -n '530,560p' apps/server/src/orchestration/decider.tsRepository: pingdotgg/t3code
Length of output: 4611
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the browser timer coercion behavior with a tiny read-only probe.
node - <<'JS'
const vals = [2_147_483_647, 2_147_483_648, 3_000_000_000, 10_000_000_000, Number.MAX_SAFE_INTEGER];
for (const v of vals) {
const n = v | 0;
console.log(JSON.stringify({
input: v,
int32: n,
unsigned: n >>> 0,
signed32: (n >> 0),
}));
}
JSRepository: pingdotgg/t3code
Length of output: 569
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the timer coercion and 32-bit boundary directly in JS semantics.
node - <<'JS'
const delays = [0, 1, 1000, 2_147_483_647, 2_147_483_648, 3_000_000_000];
for (const d of delays) {
const signed = d >> 0;
console.log(`${d} => signed32=${signed}, maxed=${Math.max(0, d)} `);
}
JSRepository: pingdotgg/t3code
Length of output: 402
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find existing timeout clamps or timer utilities in the web app.
rg -n "2147483647|max.*timeout|setTimeout\\(|clearTimeout\\(" apps/web/src | sed -n '1,220p'Repository: pingdotgg/t3code
Length of output: 9145
🌐 Web query:
window.setTimeout 2147483647 clamp browser overflow immediate fire specification
💡 Result:
The setTimeout delay parameter is limited to a maximum value of 2,147,483,647 milliseconds (the maximum value for a signed 32-bit integer) [1][2]. This limitation exists because browsers and environments like Node.js historically convert the provided delay value into a signed 32-bit integer [1][3]. When you provide a value exceeding 2,147,483,647: 1. Integer Overflow: In many browsers, providing a value greater than 2,147,483,647 can lead to integer overflow, where the value wraps around to a negative number [1][4]. Because setTimeout treats negative values as 0 (executing the callback as soon as possible in the next event cycle), the timer effectively triggers immediately [1][3][2]. 2. Environment Variations: While the underlying cause is the 32-bit limitation, behavior can vary slightly by environment [5]. For instance, Node.js explicitly documentation states that when a delay is larger than 2,147,483,647, it is set to 1 millisecond, resulting in immediate execution [6]. 3. Specification Status: Although widely implemented this way due to historical and technical constraints (often rooted in WebIDL 'long' type definitions), the HTML specification does not mandate this specific overflow behavior, nor does it explicitly define a maximum cap [7][5]. However, changing this behavior is generally avoided by implementers to prevent breaking existing legacy code that may rely on or inadvertently trigger this behavior [7][5]. To work around this limitation for delays longer than ~24.8 days, developers commonly use a recursive approach, scheduling multiple smaller, compliant timeouts to eventually trigger the callback at the desired time [3][8].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
- 2: https://dev.to/chinmaymoghe/settimeout-max-timeout-footgun-22f9
- 3: https://stackoverflow.com/questions/16314750/settimeout-fires-immediately-if-the-delay-more-than-2147483648-milliseconds
- 4: https://developer.mozilla.org/en-US/docs/Web/API/settimeout
- 5: Confused setTimout behaviour in NodeJS nodejs/node#46596
- 6: https://nodejs.org/api/timers.html
- 7: setInterval setTimeout break for interval > 2^32-1 nodejs/node#19015
- 8: https://stackoverflow.com/questions/3468607/why-does-settimeout-break-for-large-millisecond-delay-values
Clamp the snooze wake timeout to 2,147,483,647 ms
setTimeout uses a 32-bit delay, so a far-future snoozedUntil can fire much sooner than intended and keep re-arming this effect. Clamping avoids a tight timer loop for synced values outside the current UI presets.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/SidebarV2.tsx` around lines 1178 - 1187, The snooze
wake timer in the useEffect must cap delayMs at the maximum 32-bit setTimeout
delay of 2,147,483,647 ms. Preserve the existing non-negative calculation and
cleanup behavior while clamping far-future snoozedUntil values before calling
window.setTimeout.
Merge picks up project grouping (projectDisplayNameByKey, grouped pickers) and the borderless hover-action styling; the snooze/settle hover cluster now uses main's transparent button treatment. Review fixes: - Woke pill skipped on slim rows (Cursor): threadWokeAt now feeds every section, not just active cards — a thread that wakes straight into the settled tail (e.g. PR merged while snoozed) shows an amber Woke marker in its time slot and full-strength title until visited - Clamp the snooze wake timeout to 2,147,483,647 ms (CodeRabbit): far-future wakes beyond the signed-32-bit setTimeout range no longer overflow into an immediate-fire re-arm loop; the timer re-arms at the clamp until the wake is in range Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Branch updated — merged latest main (project grouping + the borderless hover-action styling; the snooze popover button and hover cluster now follow the transparent treatment) and addressed both new findings:
Typecheck + lint clean, web suite green (1479 tests), PR mergeable again. 🤖 Generated with Claude Code |
Review fixes: - decider thread.snooze (Cursor): the future-wake invariant now uses a negated comparison, so an unparseable snoozedUntil (NaN — IsoDateTime is structurally just a string) is rejected instead of persisting snooze fields that never hide the thread - mobile thread list (Cursor): buildThreadListV2Items now classifies snoozed threads via effectiveSnoozed (capability-gated per environment, snooze outranks settled — same rules as web) and hides them, reporting snoozedCount. Same account no longer shows different inbox visibility across clients; a shelf UI can consume the count later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…light fade) Woke rows are exempted from the new in-flight fade — a wake signal must not recede — and the slim-row time slot keeps the snooze wake countdown / Woke marker ahead of main's settled-vs-thread time labels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed both new Bugbot findings and re-merged main (twice — #4274's sidebar polish landed mid-update):
Merge notes: woke rows are exempted from #4274's new in-flight fade (a wake signal must not recede), and the slim-row time slot keeps the wake countdown / Woke marker ahead of the new settled-vs-thread time labels. All suites green (server 1611, web 1490, client-runtime 469, mobile 540); typecheck clean; PR mergeable. 🤖 Generated with Claude Code |
Review fixes: - SidebarV2 (Macroscope): if a thread becomes blocked while its snooze popover is open, the button unmounts without firing onOpenChange and snoozeMenuOpen stuck true — permanently hiding the status label and pinning the hover actions. The pin is now derived (raw && showSnoozeButton) and the raw state resets when the button hides, so the popover can't resurrect on remount either. - mobile thread list (Cursor ×2): snooze classification now takes a second-precise snoozeNow alongside the minute-quantized partition clock, and the layout reports nextSnoozeWakeAt; both callers arm a clamped timeout at that boundary. A woken thread reappears the moment its snooze expires instead of up to a minute late — same behavior as web. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed all three new findings:
Suites green (mobile 541, web 1490, plus server/client-runtime unchanged); typecheck clean; PR mergeable. 🤖 Generated with Claude Code |
The wake-timer effect depended only on nextSnoozeWakeAt; when a fire didn't change the boundary (clamped far-future wake, or a fire landing just before the wake instant), the chain died and the thread stayed hidden until the minute tick. snoozeWakeTick joins the deps so every fire re-arms. (Web is unaffected: its effect depends on the snoozedThreads array, which gets a fresh identity per recompute.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Fixed the timer re-arm finding: the mobile wake-timer effect now also depends on 🤖 Generated with Claude Code |
Menu cleanup per review:
- drop the "Until PR merges" / "Until next review" SOON teasers
- drop the "Snooze until" header — the presets speak for themselves
- presets carry a whenLabel for the time column that complements the
label instead of repeating it ("Tomorrow · 9:00 AM", not
"Tomorrow · tomorrow 9:00 AM"); context menus share it. The toast
keeps the full standalone description.
Merge picks up the warm-thread change-request settle guard
(CHANGE_REQUEST_SETTLE_IDLE_MS) alongside the snooze logic in
threadSettled.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When every thread in scope is snoozed, the v2 Home list and iPad sidebar showed "No threads yet" — an all-snoozed inbox read as data loss. Both empty states now consume the layout's snoozedCount and say "N threads snoozed" instead (search still outranks it; a real empty scope is unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Fixed the empty-state finding: when every thread in scope is snoozed, the mobile v2 Home list and iPad sidebar now say "N threads snoozed" (with "Snoozed threads return when their wake time passes" as the detail on Home) instead of the misleading "No threads yet". The layout's previously-unused 🤖 Generated with Claude Code |
Snooze and settle both park the thread you're done with for now, so they share forward-navigation behavior: snoozing the thread you're looking at advances to the next remaining card (skipping settled, snoozed, and same-batch rows), or a fresh draft in the project when it was the last active one. The settle navigation planner is extracted into planForwardNavigation and both actions use it; multi-select snooze passes its batch as coSnoozingKeys the same way bulk settle does. Failures never navigate, and a user navigation during the await still wins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 00e4e64. Configure here.
Review fixes: - web (Cursor): a snoozed thread opened via route (deep link, or open while snoozed elsewhere) now keeps its sidebar row under the collapsed shelf — same exception the settled tail's "Show more" makes — so the active highlight, wake affordance, and threadByKey entry survive. - mobile (Cursor): with an active search query, an empty list whose matches are all snoozed now says "All matching threads snoozed" (Home list and iPad sidebar) instead of the misleading "No results" — those threads passed the same search filter before being counted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Both new findings addressed:
Web 1505 / mobile 542 tests green, typecheck clean, mergeable. 🤖 Generated with Claude Code |

Summary
Adds an inbox-zero snooze lifecycle to Sidebar v2: hide a thread from the inbox until a chosen time, then bring it back loudly. Design follows the approved plan (option B popover entry + option C collapsed shelf + option F context-menu parity, "Woke" pill for returns): https://2udjpqoxpjoz.postplan.dev
Snooze is an overlay on the active state, not a fourth destination — crisply distinct from Settle ("done, tuck below") and Archive ("out of the sidebar"): snooze is "not now, guaranteed to come back."
Contracts + server
thread.snooze/thread.unsnoozecommands andthread.snoozed/thread.unsnoozedevents;snoozedUntil/snoozedAtonOrchestrationThread+ shell (optional fields, so payloads from pre-snooze servers still decode)thread.unsnoozed, reasonactivity)snoozed_until/snoozed_attoprojection_threads; projector, projection pipeline, and every snapshot/shell query thread the fields throughthreadSnoozecapability flag, version-skew gated exactly likethreadSettlement— clients never send the commands to older serversShared logic (client-runtime)
effectiveSnoozed: hidden while the wake time is ahead and the thread hasn't raised its hand. Timer wakes are derived — no server event fires; a passedsnoozedUntilsimply stops classifying as snoozedthreadRaisedHandWhileSnoozed: pending approval/input, a failure newer than the snooze, or a run completed after the snooze wake the thread early (classification only — the snooze fields stay put, mirroring howeffectiveSettledtreats blocked work). Snoozing a thread that already failed stays snoozed: that snooze was "I saw it, not now"threadWokeAtfeeds the woke indicator until the user visitsSidebar v2 UI
2h,18h) and a hover wake-now button; the shelf disappears entirely at count 0Tests
decider.snoozed.test.ts— invariants, idempotent re-emission, activity wake on turn.startthreadSnoozed.test.ts— effectiveSnoozed / raised-hand / canSnooze / threadWokeAt edge cases (incl. pre-snooze failures staying snoozed)Sidebar.snooze.test.ts— preset boundaries (evening suppression, Monday next-week), wake labelspnpm typecheck,pnpm lint(no new warnings), and full test suites for server (1571 passed), web (1444), client-runtime (467), contracts (187) all green. Codex review ran on the diff; its one finding (pre-existing failures instantly un-hiding a fresh snooze) is fixed and covered by a test.🤖 Generated with Claude Code
Note
Medium Risk
Touches orchestration commands, projection schema, and list partitioning on web and mobile; invariants mirror settle but new lifecycle paths (activity wake, raised-hand classification) add behavioral surface area.
Overview
Adds thread snoozing end-to-end: hide threads until a future wake time, with visibility and inbox behavior aligned to settlement patterns but without pausing the agent.
Server & persistence: New
thread.snooze/thread.unsnoozecommands and matching events;snoozedUntil/snoozedAton threads via migration 034 and projection plumbing. The decider enforces future wake times, blocks snooze when there are open approvals/user-input or a queued turn start, supports idempotent re-snooze, and unsnoozes on user message (activity).threadSnoozecapability is advertised likethreadSettlement.Shared client logic:
effectiveSnoozed,canSnooze,threadRaisedHandWhileSnoozed, andthreadWokeAt— timer wakes are client-derived (no wake event); blocked work, new failures, or post-snooze completions can surface threads early without clearing snooze fields.Web Sidebar v2: Preset snooze popover and context menus, collapsible Snoozed shelf, wake countdowns, Woke pill until visit, composer banner for snoozed active thread with Wake now, and forward navigation when snoozing the open thread (shared with settle).
Mobile: Thread list v2 hides snoozed threads (capability-gated), arms
setTimeoutonnextSnoozeWakeAtfor second-precise re-partition, and improves empty states when the inbox is all-snoozed.Reviewed by Cursor Bugbot for commit efdf69b. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add thread snoozing to sidebar with preset durations, wake indicators, and server persistence
setTimeout.thread.snooze/thread.unsnoozecommands are added to the orchestration protocol, with server-side validation (future wake time, no blocking requests), idempotent event emission, and automatic unsnooze on user message.snoozed_untilandsnoozed_atcolumns toprojection_threads; thethreadSnoozecapability flag gates the feature per environment.Macroscope summarized efdf69b.
Summary by CodeRabbit